/*
 * Copyright (c)  2020,  DataPipeline
 * All rights reserved.
 *
 * Id:ClassPathLoader.java   2020-07-16 00:09 wanglijun
 */
package cn.datacare.orm.loader;

import cn.datacare.orm.conversion.DefaultSqlIdNameConversion;
import cn.datacare.orm.conversion.SqlIdNameConversion;
import cn.datacare.orm.dialect.Dialect;
import cn.datacare.orm.dialect.MySQLDialect;
import cn.datacare.orm.manager.impl.SQLManager;
import cn.datacare.orm.source.SQLSource;
import cn.datacare.orm.source.SQLTableSource;
import cn.datacare.orm.util.IOUtils;
import cn.datacare.orm.util.Separator;
import cn.datacare.orm.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static cn.datacare.orm.constant.Constants.*;

/**
 * <p>
 * Title: ClassPath加载SQL语句
 * </p>
 * <p>
 * Description: 从classpath系统加载sql模板，id应该格式是"xx.yyy",xx代表了文件名，yyy代表了sql标识 sql 模板格式如下：
 * </p>
 * <p>
 * Copyright: Copyright (c) 2020
 * </p>
 * <p>
 * Company/Department: DataPipeline
 * </p>
 *
 * @author wanglijun
 * @version 1.0
 * @since 2020-07-16 00:09
 */
public class ClassPathLoader implements SQLLoader {
    /***
     * 日志
     */
    private static final Logger log = LoggerFactory.getLogger(ClassPathLoader.class);
    /**
     * SQL根目录
     */
    protected String sqlRoot = null;
    /**
     * 文件加载
     */
    protected Map<String, SQLSource> sqlSourceMap = new ConcurrentHashMap<>();

    /***
     * 数据库方言
     */
    protected Dialect dialect = null;

    /***
     * 是否自动检查
     */
    protected boolean autoCheck = Boolean.TRUE;
    /**
     * 字符编码，默认为UTF-8
     */
    protected String charset = StandardCharsets.UTF_8.name();
    /***
     * 设置变量
     */
    protected SQLSource NO_EXIST = new SQLSource();
    /***
     * 类型加载器
     */
    protected ClassLoader classLoader = null;
    protected SQLManager sqlManager = null;
    /***
     * SQLIdNameConversion 转换规则
     */
    private SqlIdNameConversion sqlIdNameConversion = new DefaultSqlIdNameConversion();

    /**
     * 初始化
     */
    public ClassPathLoader() {
        this(DEFAULT_SQL_ROOT);
    }

    /**
     * 数据目录
     *
     * @param sqlRoot
     */
    public ClassPathLoader(String sqlRoot) {
        this(sqlRoot, new MySQLDialect());
    }

    /***
     *
     * @param sqlRoot SQL根目录
     * @param dialect 数据库类型
     */
    public ClassPathLoader(String sqlRoot, Dialect dialect) {
        this.sqlRoot = sqlRoot;
        this.dialect = dialect;
    }

    /***
     * 获取URLVersion
     * @param url url
     * @return Long
     */
    protected static Long getURLVersion(URL url) {
        if (url == null) {
            return 0L;
        }

        if (FILE.equals(url.getProtocol())) {
            String path = url.getFile();
            return new File(path).lastModified();
        } else {
            //其他协议，比如jar。
            return 0L;
        }
    }

    /**
     * 加载sql，如果未找到，抛出错误
     *
     * @param id
     * @return
     */
    @Override
    public SQLSource getSQL(String id) {
        return this.tryLoadSQL(id);
    }

    /**
     * 尝试加载SQL
     *
     * @param id
     * @return
     */
    private SQLSource tryLoadSQL(String id) {
        SQLSource sqlSource = sqlSourceMap.get(id);
        boolean hasLoad = false;
        if (sqlSource == null) {
            this.loadSQL(id);
            hasLoad = true;
        }
        if (!hasLoad && this.autoCheck && isModified(id)) {
            this.loadSQL(id);
        }

        //处理完后再次获取
        sqlSource = sqlSourceMap.get(id);
        if (sqlSource == null) {
            sqlSourceMap.put(id, this.NO_EXIST);
            return null;
        } else if (sqlSource == this.NO_EXIST) {
            return null;
        } else {
            return sqlSource;
        }
    }

    /***
     *  考虑到跨数据库支持，ClasspathLoader加载SQL顺序如下：
     首先根据DBStyle.getName() 找到对应的数据库名称，然后在ROOT/dbName 下找对应的sql，
     如果ROOT/dbName 文件目录不存在，或者相应的sql文件不存在，再搜索ROOT目录下的sql文件。
     如mysql 里查找user.select2,顺序如下：
     - 先找ROOT/mysql/user.sql 文件，如果有此文件，且包含了select2，则返回此sql语句，
     - 如果没有，下一步查找ROOT/mysql/user.md,如果有此文件，且包含了slect2，则返回sql语句
     - 如果没有，下一步查找ROOT/user.sql,如果有此文件，且包含了slect2，则返回sql语句
     - 如果没有，下一步查找ROOT/user.md,如果有此文件，且包含了slect2，则返回sql语句
     - 都没有，抛错，告诉用户未在ROOT/,或者ROOT/mysql 下找到相关sql
     *
     */
    private boolean loadSQL(String id) {
        //读取root目录下的sql文件
        URL ins = this.getRootFile(id);
        boolean rootResult;
        rootResult = this.readSQLFile(id, ins, true);
        //读取db目录下的sql文件，进行覆盖
        ins = this.getDialectRootFile(id);
        boolean dbResult;
        dbResult = this.readSQLFile(id, ins, false);
        return rootResult || dbResult;
    }

    private boolean readSQLFile(String id, URL url, boolean isRoot) {
        if (url == null) {
            return false;
        }
        InputStream ins;
        try {
            ins = url.openStream();
        } catch (IOException e1) {
            return false;
        }
        String modelName = id.substring(0, id.lastIndexOf(Separator.DOT) + 1);
        if (ins == null) {
            return false;
        }

        long lastModified = getURLVersion(url);


        BufferedReader bf = null;
        try {

            bf = new BufferedReader(new InputStreamReader(ins, charset));
            MarkDownParser markDownParser = new MarkDownParser(modelName, bf);
            SQLSource source = null;
            while ((source = markDownParser.next()) != null) {
                SQLFileVersion version = new SQLFileVersion();
                version.url = url;
                if (isRoot) {
                    version.root = lastModified;
                } else {
                    version.db = lastModified;
                }
                source.setVersion(version);
                sqlSourceMap.put(source.getId(), source);
            }

        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(bf);
        }
        return true;
    }

    /***
     * 加载文件
     * @param filePath
     * @return
     */
    private URL getFile(String filePath) {

        URL url = null;
        if (classLoader != null) {
            url = classLoader.getResource(filePath);
            if (url != null) {
                return url;
            }
        }

        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader != null) {
            url = loader.getResource(filePath);
            if (url != null) {
                return url;
            } else {
                url = this.getClass().getResource(filePath);
                return url;
            }
        } else {
            url = this.getClass().getResource(filePath);
            return url;
        }


    }

    /***
     * 获取数据
     * @param id ID
     * @return URL
     */
    private URL getDialectRootFile(String id) {
        String path = this.sqlIdNameConversion.getPath(id);
        Path filePath = this.buildPath(this.dialect.getName(), path, SQL_SUFFIX_SQL);
        URL is = this.getFile(filePath.getFileName().toAbsolutePath().toString());
        if (is == null) {
            filePath = this.buildPath(this.dialect.getName(), path, SQL_SUFFIX_MD);
            is = this.getFile(filePath.getFileName().toAbsolutePath().toString());
        }
        return is;
    }

    /***
     * 构建Path
     * @param path  Path
     * @param extSuffix 文件名
     * @return Path
     */
    private Path buildPath(String path, String extSuffix) {
        return this.buildPath(null, path, extSuffix);
    }

    /***
     * 构建Path
     * @param dialectName 文件名
     * @param path  Path
     * @param extSuffix 文件名
     * @return Path
     */
    private Path buildPath(String dialectName, String path, String extSuffix) {
        dialectName = StringUtils.isNotEmpty(dialectName) ? dialectName : StringUtils.EMPTY;
        return Paths.get(this.sqlRoot, dialectName, path + extSuffix);
    }

    /***
     * 获取数据
     * @param id ID
     * @return URL
     */
    private URL getRootFile(String id) {
        String strPath = this.sqlIdNameConversion.getPath(id);
        Path filePath = this.buildPath(strPath, SQL_SUFFIX_SQL);
        URL is = this.getFile(filePath.getFileName().toAbsolutePath().toString());
        if (is == null) {
            filePath = this.buildPath(strPath, SQL_SUFFIX_MD);
            is = this.getFile(filePath.getFileName().toAbsolutePath().toString());
        }
        return is;
    }

    /**
     * 判断一个sql是否修改过
     *
     * @param id ID
     * @return boolean
     */
    @Override
    public boolean isModified(String id) {
        SQLSource source = this.sqlSourceMap.get(id);
        if (source != null && source instanceof SQLTableSource && source == NO_EXIST) {
            return false;
        }

        long oldRootVersion = source.getVersion().root;
        long oldDbVersion = source.getVersion().db;


        //如果db目录中有sql文件，直接使用db目录的文件判断版本（root中的文件会被db中的覆盖）
        URL root = this.getRootFile(id);
        URL db = this.getDialectRootFile(id);

        //如果root目录和db目录只要有一个变化，都认为sql文件变化，重新加载
        return getURLVersion(root) != oldRootVersion || getURLVersion(db) != oldDbVersion;
    }

    /**
     * 判断资源是否存储
     *
     * @param id ID
     * @return boolean
     */
    @Override
    public boolean existedResource(String id) {
        return getRootFile(id) != null || this.getDialectRootFile(id) != null;

    }

    /**
     * 判断一个sql是否存在
     *
     * @param id Id
     * @return boolean
     */
    @Override
    public boolean existed(String id) {
        return this.tryLoadSQL(id) != null;
    }

    /**
     * SQLLoader里增加一个自动生成的SQL
     *
     * @param id
     * @param sqlSource
     */
    @Override
    public void addSQL(String id, SQLSource sqlSource) {
        sqlSourceMap.put(id, sqlSource);
    }

    /**
     * 每次都检测sql变化
     *
     * @return
     */
    @Override
    public boolean isAutoCheck() {
        return this.autoCheck;
    }

    /**
     * 设置是否检测SQL变化，开发模式下检查
     *
     * @param autoCheck
     */
    @Override
    public void setAutoCheck(boolean autoCheck) {
        this.autoCheck = autoCheck;
    }

    @Override
    public String getCharset() {
        return this.charset;
    }

    @Override
    public void setCharset(String charset) {
        this.charset = charset;
    }

    /**
     * sqlId到sql文件的转化
     *
     * @param sqlIdNameConversion
     */
    @Override
    public void setSQLIdNameConversion(SqlIdNameConversion sqlIdNameConversion) {
        this.sqlIdNameConversion = sqlIdNameConversion;
    }

    /**
     * 设置当前使用的数据库，以让sqlloder优先寻找数据库
     *
     * @param dialect
     */
    @Override
    public void setDialect(Dialect dialect) {
        this.dialect = dialect;

    }

    @Override
    public SQLManager getSQLManager() {
        return this.sqlManager;
    }

    @Override
    public void setSQLManager(SQLManager sqlManager) {
        this.sqlManager = sqlManager;
    }

    /***
     * 刷新数据
     */
    @Override
    public void refresh() {
        if (this.sqlSourceMap != null) {
            this.sqlSourceMap.clear();
        }
        //创建数据
        this.sqlSourceMap = new ConcurrentHashMap<>(16);
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /***
     * SQLFileVersion 定义
     */
    public static class SQLFileVersion {
        /**
         * URL
         */
        public URL url;
        /**
         * 根目录下sql文件版本
         */
        public long root = 0L;
        /**
         * 具体db下的
         */
        public long db = 0L;

        /***
         * 是否修改文件
         * @param newVersion
         * @return boolean
         */
        public boolean isModified(SQLFileVersion newVersion) {
            return newVersion.root != root || newVersion.db != db;
        }

    }
}
